 module GridHelper
  class Grid
    
    class GridColumn
      attr_accessor :fld, :options, :lines
      def initialize(grid, fld, options)
        @grid = grid
        @fld = fld
        @options = options
        @options[:separator] ||= "<br/>"
        @lines = []
      end
      
      def line(*args)
        options = args.last.is_a?(Hash) ? args.pop : {}
        fld = args.shift
        line = GridLine.new(fld, options)
        yield line if block_given?
        @lines << line
      end
      
      def value(element, first_col = false)
        if @lines.empty?
          text = format_data(multi_send(element, @fld), @options)
          first_col ? link_to_element(element, text) : text
        else
          first_line = true
          @lines.map do |line| 
            line_value = line.value(element)
            text = (first_col && first_line) ? link_to_element(element, line_value) : line_value
            first_line = false
            format_data(text, line.options)
            end.join(@options[:separator])
          end
        end
        
        def link_to_element(element, text)
          parent = @grid.opciones[:parent]
          if @grid.opciones[:no_autolink]
            rtn = text
          else
            autolink = @grid.opciones[:autolink]
            if autolink.is_a?(Proc) 
              rtn = autolink.call(element)
            end
            rtn ||= @grid.template.link_to(text, @grid.template.my_url_for(@grid.controller, 'show', element, parent))
          end
          rtn
        end
        
        def format_data(data, options)
          if data.is_a?(Date)
            data.to_time.strftime('%Y-%m-%d')
          elsif data.is_a?(Time)
            data.strftime('%Y-%m-%d %H:%M')
          elsif data.is_a?(TrueClass) || data.is_a?(FalseClass)
            data ? 'Sí' : 'No'
          elsif options[:format] == :currency
            data ? @grid.template.number_to_currency(data, :precision => (options[:precision] || 2)) : ''
          elsif options[:format] == :numeric
            @grid.template.number_with_precision(data || 0, options[:precision] || 2)
          else
            data
          end
        end
        
      end
      
      class GridLine
        attr_accessor :fld, :options, :fields
        def initialize(fld, options = {})
          @fld = fld
          @options = options
          @options[:separator] ||= ", "
          @fields = []
        end
        
        def field(*args)
          options = args.last.is_a?(Hash) ? args.pop : {}
          fld = args.shift
          field = GridField.new(fld, options)
          @fields << field
        end
        
        def value(element)
          v = if @fields.empty?
            multi_send(element, @fld)
          else
            @fields.map {|l| l.value(element)}.join(@options[:separator])
          end
          v = %{<span class="#{@options[:class]}">#{v}</span>} if @options[:class]
          v
        end
      end
      
      class GridField
        attr_accessor :fld, :options
        def initialize(fld, options = {})
          @fld = fld
          @options = options
        end
        
        def value(element)
          cls = @options[:class] ? %{ class="#{@options[:class]}"} : ''
        "<span#{cls}>#{multi_send(element, @fld)}</span>"
        end
      end
      
      attr_reader :columns, :modelo, :lista, :opciones, :controller, :controller_name, :controller_class, :template
      def initialize(template, modelo, lista, opciones = {})
        @template, @modelo, @lista = template, modelo, lista
        @opciones = opciones.reverse_merge :sortable => true, :edit => true, :new_link => true, :actions => true
        
        @columns = []
        @controller = opciones[:controller] || @modelo.to_s.pluralize
        @controller_name = (@controller + '_controller').to_sym
        @controller_class = Object::const_get(@controller_name.to_s.classify) rescue nil
      end
      
      def column(*args)
        options = args.last.is_a?(Hash) ? args.pop : {}
        field = args.shift
        options[:sortable] = @opciones[:sortable] if options[:sortable].nil? && field.is_a?(Symbol)
        options[:sortable] = false if block_given? || options[:value]
        unless options[:label]
          options[:label] = (field.is_a?(Array) ? field.first : field).to_s.humanize
        end
        col = GridColumn.new(self, field, options)
        yield col if block_given?
        @columns << col
      end
      
      def helper(field, opciones = {})
        column field, opciones.merge(:value => lambda {|e| @template.send(field, e)})
      end
    end 
    
    def grid_for(*args, &block)
      modelo, lista, opciones = grid_parameters(*args)
      block ||= opciones[:bloque]
      block.call(g = Grid.new(@template, modelo, lista, opciones))
      grid_to_html(g)
    end
    
    private
    
    def grid_to_html(g)
      header_html(g) + if g.lista
        table_html(g) + paginator_html(g)
      else
        content_tag(:div, "No hay registros para mostrar.")
      end
    end
    
    def header_html(g)
      if g.opciones[:header] == false
        header = content_tag(:div, model_spinner(g) + header_new_url(g), :class => 'titulo_centrado')
      else
        header = g.opciones[:header] || g.modelo.to_s.pluralize.humanize
        header << model_spinner(g) unless g.opciones[:new_link]
        new_url = header_new_url(g)
        header = content_tag(:div, content_tag(:h3, header) + new_url, :class => 'titulo_centrado')
      end
      header << header_inline_form(g)
    end
    
    def header_new_url(g)
      url = if g.opciones[:header] == false || !g.opciones[:new_link] 
        content_tag(:div, '', :id => new_link_id(g)) 
      else 
        link_to_new_url(g)
      end
      txt = button_text(g.controller_class, 'Cancelar', :cancel)
      url <<  link_to_cancel(txt, :remote => :true, :model => g.modelo, :hidden => true) 
      url || "que carajo pasa!" #TODO: Remover urgente!
    end
   
    def link_to_new_url(g)
      url = my_url_for(g.controller, 'new', nil, g.opciones[:parent])
      txt = g.opciones[:new_link_text] || "Agregar #{g.modelo.to_s.humanize}"
      content_tag(:div, 
        link_to_remote(txt, 
          toggle_spinner(model_spinner_id(g), url, :href => url)) + model_spinner(g),
          :id  => new_link_id(g))
    end
    
    def model_spinner(g)
      spinner(:id => model_spinner_id(g), :class => 'new_link_spinner')
    end
    
    def new_link_id(g); "#{g.modelo}_new_link"; end
    def model_spinner_id(g); "#{g.modelo}_spinner"; end
    
    def header_inline_form(g)
      if g.opciones[:new_link] || g.opciones[:edit]
        content_tag(:div, '&nbsp;', :id => "#{g.modelo}_inline_form", :style => 'display:none')
      else
        ''
      end
    end
    
    def table_html(g)
      content_tag(:table, cabecera(g) + filas(g), :class => 'forums' )
    end
    
    def paginator_html(g)
      paginator = instance_variable_get("@#{g.modelo.to_s.pluralize}_paginator")
      return '' unless paginator && paginator.page_count > 1
      link_prev, link_next = paginator_prev_and_next(paginator)
      pag_links = pagination_links_remote(paginator)
      rtn = content_tag(:span, 
            "Páginas: <strong>#{link_prev}#{pag_links}#{link_next}</strong>",
            :class => 'pages')
      rtn << content_tag(:span,
            "Mostrando #{paginator.current.first_item} - #{paginator.current.last_item} de #{paginator.item_count}",
            :class => 'pages_right')
      rtn << "<br/>" + content_tag(:span, link_to('Ver todos los registros', :no_paginate => 1),
            :class => 'pages')
    end
    
    def paginator_prev_and_next(paginator)
      link_prev = if paginator.current.previous
        link_to_remote(h('< Anterior'), :url => {:action => 'index', :page => paginator.current.previous}, :method => :get) + " | "
      else
        ''
      end
      link_next = if paginator.current.next
        ' | ' + link_to_remote(h('Siguiente >'), :url => {:action => 'index', :page => paginator.current.next}, :method => :get)
      else
        ''
      end
      [link_prev, link_next]
    end
    
    def pagination_links_remote(paginator)
      pagination_links_each(paginator, :window_size => 4) do |n|
        options = {:url => {:action => 'index', :page => n}, :method => :get}
        link_to_remote n.to_s, options
      end
    end
    
    def cabecera(g)
      content_tag('tr', titulos(g))
    end
    
    def titulos(g)
      rtn = []
      g.columns.each do |col|
        rtn << content_tag('th', titulo_columna(g, col)) 
      end
      if actions_column?(g)
        rtn << content_tag('th', 'Acciones') 
      end
      rtn.join("\n")
    end
    
    def titulo_columna(g, col)
      texto = if col.options[:sortable]
        order_key = "order_#{g.modelo}".to_sym
        orden = (actual(g, col.fld) && session[order_key] != " DESC") ? "down" : "up"
        link_to_remote(col.options[:label], toggle_spinner("#{g.modelo}_spinner", sort_url(g, col, orden)))
      else
        orden = nil
        col.options[:label]
      end
      content_tag 'span', texto, :class => class_titulo_columna(g, col.fld, orden)
    end
    
    def sort_url(g, col, orden)
      controller.my_url_for_hash(g.controller, 'index', nil, g.opciones[:parent], 
      :sort_by => col.fld, :order => orden, 
      :filter => session["#{g.modelo}_filter".to_sym])
    end
    
    def actual(g, field)
      sort_key = "sort_by_#{g.modelo}".to_sym
      session[sort_key] == field.to_s
    end
    
    def class_titulo_columna(g, field, orden)
      if orden
        "sort_#{actual(g, field) ? orden : 'none'}"
      else
        "not_sort"
      end
    end
    
    def filas(g)
      g.lista.map{|e| fila(g, e)}.join
    end
    
    def fila(g, elemento)  
      fields = []
      first_col = true
      actual = (params[:id] == elemento.id)
      
      for col in g.columns  
        clase = field_class(g, col)
        if valor = col.options[:value]
          valor = valor.call(elemento) if valor.is_a?(Proc)
        else
          valor = col.value(elemento, first_col)
        end
        first_col = false
        texto = valor
        if params[:search] && g.controller_class.search_fields.include?(col.fld)
          texto = highlight(texto, params[:search]) 
        end
        
        fields << content_tag(:td, texto, :class => clase)
      end
      if actions_column?(g)
        fields << content_tag(:td, action_links(g, elemento)) 
      end
      content_tag(:tr, fields.join("\n"), :class => "hentry #{' marcar' if actual}", :id => elemento.id_) 
    end
    
    def field_class(g, col)
      cls = actual(g, col.fld) ? "inv" : nil
      is_numeric = [:currency, :numeric].include?(col.options[:format])
      cls = cls.to_s + (is_numeric ? " ra" : "")
      cls.empty? ? nil : cls
    end
    
    def action_links(g, elemento)
      actions = []
      parent = g.opciones[:parent]
      editable = if g.opciones[:edit].is_a?(Proc)
        g.opciones[:edit].call(elemento)
      else
        g.opciones[:edit]
      end
      if editable
        url = my_url_for_hash(g.controller, 'edit', elemento, parent).merge :inline => true
        link = link_to_remote('Editar', toggle_spinner("#{g.modelo}_spinner", url))
        actions << content_tag(:span, link, :class => 'edit_link')
      end
      unless g.opciones[:actions] == false
        actions << action_links_array(elemento, :model => g.modelo, :controller_name => g.controller_name).map {|link| content_tag(:span, link, :class => 'action_link')}
      end
      actions.join(' ')    
    end
    
    def actions_column?(g)
      g.controller_class && g.opciones[:actions] && (!g.controller_class.actions.empty? || g.opciones[:edit])
    end
    
  end
  
  ActionView::Base.send :include, GridHelper